/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;

namespace Xceed.Silverlight.Data
{
  public sealed class FilterExpressionPreProcessor : FilterExpressionVisitor
  {
    #region Constructor

    public FilterExpressionPreProcessor(
      FilterExpressionBase expression,
      DataSourceProvider dataSourceProvider )
      : base( expression )
    {
      if( dataSourceProvider == null )
        throw new ArgumentNullException( "dataSourceProvider" );

      m_getItemMemberType = dataSourceProvider.GetElementMemberType;
    }

    internal FilterExpressionPreProcessor(
      FilterExpressionBase expression,
      Func<string, Type> getItemMemberType )
      : base( expression )
    {
      if( getItemMemberType == null )
        throw new ArgumentNullException( "getItemMemberType" );

      m_getItemMemberType = getItemMemberType;
    }

    #endregion

    public FilterExpressionPreProcessorResult GetResult()
    {
      this.Visit();

      if( ( m_result != null ) && ( m_result.Target == PreProcessResult.TargetSome ) )
      {
        FilterExpressionBase expression = m_result.Expression;

        // Simplify the FilterExpression as most as possible.
        if( expression != null )
        {
          FilterExpressionSimplifier simplifier = new FilterExpressionSimplifier( expression );
          expression = simplifier.GetResult();

          m_result = new FilterExpressionPreProcessorResult( expression );
        }
      }

      return m_result;
    }

    protected override void Initialize()
    {
      m_result = null;

      base.Initialize();
    }

    protected internal override void Visit( FilterExpression expression )
    {
      object value = expression.Value;

      if( value != null )
      {
        Type memberType = this.GetItemMemberType( expression.MemberName );
        Type valueType;
        value = FilterExpressionPreProcessor.GetConvertedValue( value, memberType, out valueType );

        FilterExpressionPreProcessorResult result = FilterExpressionPreProcessor.GetResult( expression, expression.FilterOperator, value );
        Debug.Assert( ( result.Expression != null ) && ( result.Expression is FilterExpression ) );

        expression = ( FilterExpression )result.Expression;

        // If the filter expression is time-insensitive, we must transformed it into a
        // time sensitive filter expression before processing it.
        if( ( typeof( DateTime ).IsAssignableFrom( value.GetType() ) )
          && ( !expression.IsTimeSensitive.GetValueOrDefault( true ) ) )
        {
          FilterExpressionBase newExpression = FilterExpressionPreProcessor.TransformTimeInsensitiveFilterExpression( expression );

          // Process the new time-sensitive expression.
          this.Visit( newExpression );
          return;
        }
      }

      m_result = this.Expand( expression );
    }

    protected internal override void Visit( AndFilterExpression expression )
    {
      List<FilterExpressionBase> operands = new List<FilterExpressionBase>();

      foreach( FilterExpressionBase childExpression in expression.ChildExpressions )
      {
        this.Visit( childExpression );

        // Retrieve the result of the current operand.
        FilterExpressionPreProcessorResult operand = m_result;

        // An operand that returns no item makes the and-expression totally useless.
        if( operand.Target == PreProcessResult.TargetNone )
          return;

        // An operand that returns all items shouldn't be consider since it doesn't bring
        // anything new to the and-expression.
        if( operand.Expression != null )
        {
          operands.Add( operand.Expression );
        }
      }

      int count = operands.Count;

      // The expression hasn't changed at all.
      if( ( expression.ChildExpressions.Count == count )
        && ( expression.ChildExpressions.SequenceEqual( operands ) ) )
      {
        m_result = new FilterExpressionPreProcessorResult( expression );
      }
      // The and-expression returns all items.
      else if( count == 0 )
      {
        m_result = FilterExpressionPreProcessorResult.TargetAll;
      }
      // The and-expression has only one operand.
      else if( count == 1 )
      {
        m_result = new FilterExpressionPreProcessorResult( operands[ 0 ] );
      }
      else
      {
        m_result = new FilterExpressionPreProcessorResult( new AndFilterExpression( operands ) );
      }
    }

    protected internal override void Visit( OrFilterExpression expression )
    {
      List<FilterExpressionBase> operands = new List<FilterExpressionBase>();

      foreach( FilterExpressionBase childExpression in expression.ChildExpressions )
      {
        this.Visit( childExpression );

        // Retrieve the result of the current operand.
        FilterExpressionPreProcessorResult operand = m_result;

        // An operand that returns all items makes the or-expression totally useless.
        if( operand.Target == PreProcessResult.TargetAll )
          return;

        // An operand that returns no item shouldn't be consider since it doesn't bring
        // anything new to the or-expression.
        if( operand.Expression != null )
        {
          operands.Add( operand.Expression );
        }
      }

      int count = operands.Count;

      // The expression hasn't changed at all.
      if( ( expression.ChildExpressions.Count == count )
        && ( expression.ChildExpressions.SequenceEqual( operands ) ) )
      {
        m_result = new FilterExpressionPreProcessorResult( expression );
      }
      // The or-expression returns no item.
      else if( count == 0 )
      {
        m_result = FilterExpressionPreProcessorResult.TargetNone;
      }
      // The or-expression has only one operand.
      else if( count == 1 )
      {
        m_result = new FilterExpressionPreProcessorResult( operands[ 0 ] );
      }
      else
      {
        m_result = new FilterExpressionPreProcessorResult( new OrFilterExpression( operands ) );
      }
    }

    protected internal override void Visit( NotFilterExpression expression )
    {
      this.Visit( expression.ChildExpression );

      // Retrieve the result of the visited nodes.
      FilterExpressionPreProcessorResult result = m_result;
      FilterExpressionBase childExpression = result.Expression;

      // The expression hasn't changed at all.
      if( childExpression == expression.ChildExpression )
      {
        m_result = new FilterExpressionPreProcessorResult( expression );
      }
      else if( childExpression != null )
      {
        m_result = new FilterExpressionPreProcessorResult( new NotFilterExpression( childExpression ) );
      }
    }

    private static object GetConvertedValue( object value, Type targetType, out Type valueType )
    {
      if( value != null )
      {
        valueType = targetType;

        // Get the real type under the Nullable<>.
        if( valueType.IsNullableStruct() )
        {
          valueType = Nullable.GetUnderlyingType( valueType );
        }

        // Converting to type object is useless.
        if( valueType != typeof( object ) )
        {
          Type type = value.GetType();

          // Try to convert the value only if an implicit cast isn't available.
          if( !valueType.IsAssignableFrom( type ) )
          {
            try
            {
              value = Convert.ChangeType( value, valueType, null );
            }
            catch( Exception )
            {
              // The exception is swallowed intentionally.
              // The conversion failed.
              valueType = type;
            }
          }
        }
      }
      else
      {
        valueType = typeof( object );
      }

      return value;
    }

    private static FilterExpressionBase TransformTimeInsensitiveFilterExpression( FilterExpression expression )
    {
      // Note: In all case, we must return a FilterExpression that is time-sensitive to avoid reentrancy.
      Debug.Assert( expression.Value is DateTime );

      // Remove the time part from the date value.
      DateTime value = ( DateTime )expression.Value;
      value = new DateTime( value.Year, value.Month, value.Day );

      string memberName = expression.MemberName;

      // When dealing with DateTime value with IsTimeSensitive set to false,
      // we must potentialy manipulate the FilterExpression so that the Time
      // part is excluded.
      switch( expression.FilterOperator )
      {
        case FilterOperator.Equal:
          {
            // In the case of "Equal", the date must be greater or equal to the date and less than the date + 1 day.
            return new AndFilterExpression(
              new FilterExpression( memberName, FilterOperator.GreaterThanOrEqual, value, true ),
              new FilterExpression( memberName, FilterOperator.LessThan, value.AddDays( 1d ), true ) );
          }

        case FilterOperator.NotEqual:
          {
            // In the case of "NotEqual", the date must be less than the date and greater or equal to the date + 1 day.
            return new OrFilterExpression(
              new FilterExpression( memberName, FilterOperator.LessThan, value, true ),
              new FilterExpression( memberName, FilterOperator.GreaterThanOrEqual, value.AddDays( 1d ), true ) );
          }

        case FilterOperator.GreaterThan:
          {
            // In the case of "GreaterThan", the date must be greater or equal than the date + 1 day.
            return new FilterExpression( memberName, FilterOperator.GreaterThanOrEqual, value.AddDays( 1d ), true );
          }

        case FilterOperator.LessThanOrEqual:
          {
            // In the case of "LessThanOrEqual", the date must be less than the date + 1 day.
            return new FilterExpression( memberName, FilterOperator.LessThan, value.AddDays( 1d ), true );
          }

        default:
          {
            return new FilterExpression( memberName, expression.FilterOperator, value, true );
          }
      }
    }

    private static FilterExpressionPreProcessorResult GetResult(
      FilterExpression expression,
      FilterOperator filterOperator,
      object value )
    {
      FilterExpression newExpression = expression;

      if( ( expression.FilterOperator != filterOperator ) || ( expression.Value != value ) )
      {
        newExpression = ( FilterExpression )expression.Clone();
        newExpression.FilterOperator = filterOperator;
        newExpression.Value = value;
      }

      return new FilterExpressionPreProcessorResult( newExpression );
    }

    private FilterExpressionPreProcessorResult Expand( FilterExpression expression )
    {
      //
      // Here is an example of a valid expression:
      //
      // item.ShipCity > value
      // 
      // Since ShipCity may be "null", some comparison operators may not work
      // on some providers.  We will consider the "null" value as the lowest
      // possible value.  In order to get the desired items, some expressions
      // need to be converted to another expression.
      //
      // If the value parameter is "null", here are the resulting expressions:
      // 
      // * item.ShipCity > value   =>  item.ShipCity != null
      // * item.ShipCity >= value  =>  (no filtering needed)
      // * item.ShipCity < value   =>  (filter all)
      // * item.ShipCity <= value  =>  item.ShipCity == null
      //
      // If the value parameter is not "null", here are the resulting expressions:
      //
      // * item.ShipCity > value   =>  item.ShipCity > value
      // * item.ShipCity >= value  =>  item.ShipCity >= value
      // * item.ShipCity < value   =>  item.ShipCity == null || item.ShipCity < value
      // * item.ShipCity <= value  =>  item.ShipCity == null || item.ShipCity <= value

      object value = expression.Value;

      if( value == null )
        return this.HandleNullValue( expression );

      Type valueType = value.GetType();

      if( typeof( bool ).IsAssignableFrom( valueType ) )
        return this.HandleBooleanValue( expression );

      if( typeof( IComparable ).IsAssignableFrom( valueType ) )
        return this.HandleComparableValue( expression );

      if( ( expression.FilterOperator == FilterOperator.Equal )
        || ( expression.FilterOperator == FilterOperator.NotEqual ) )
        return this.HandleEquatableValue( expression );

      throw new InvalidOperationException( "An attempt was made to use an unsupported filter expression." );
    }

    private FilterExpressionPreProcessorResult HandleNullValue( FilterExpression expression )
    {
      switch( expression.FilterOperator )
      {
        case FilterOperator.Equal:
        case FilterOperator.LessThanOrEqual:
          {
            Type memberType = this.GetItemMemberType( expression.MemberName );

            // Make sure the member type can be compare to null.
            if( memberType.IsNullableType() )
              return this.CompareEqualNull( expression );

            return FilterExpressionPreProcessorResult.TargetNone;
          }

        case FilterOperator.NotEqual:
        case FilterOperator.GreaterThan:
          {
            Type memberType = this.GetItemMemberType( expression.MemberName );

            // Make sure the member type can be compare to null.
            if( memberType.IsNullableType() )
              return this.CompareNotEqualNull( expression );

            return FilterExpressionPreProcessorResult.TargetAll;
          }

        case FilterOperator.GreaterThanOrEqual:
          {
            // All possible values are greater than or equal to null.
            return FilterExpressionPreProcessorResult.TargetAll;
          }

        case FilterOperator.LessThan:
          {
            // No value can be less than null.
            return FilterExpressionPreProcessorResult.TargetNone;
          }

        case FilterOperator.Contains:
        case FilterOperator.ContainedIn:
        case FilterOperator.StartsWith:
        case FilterOperator.EndsWith:
          {
            // No value may starts, ends or contains the null value.
            return FilterExpressionPreProcessorResult.TargetNone;
          }

        default:
          throw new InvalidOperationException( "An attempt was made to use an unsupported filter expression." );
      }
    }

    private FilterExpressionPreProcessorResult HandleBooleanValue( FilterExpression expression )
    {
      bool value = ( bool )expression.Value;

      // Handle the comparison of boolean values.  Here is the order considered
      // for boolean values: null < false < true.
      // 
      switch( expression.FilterOperator )
      {
        case FilterOperator.Equal:
          {
            return this.CompareEqual( expression, value );
          }

        case FilterOperator.NotEqual:
          {
            return this.CompareNotEqual( expression, value );
          }

        case FilterOperator.GreaterThan:
          {
            // No value is greater than true.
            if( value )
              return FilterExpressionPreProcessorResult.TargetNone;

            return this.CompareEqual( expression, true );
          }

        case FilterOperator.GreaterThanOrEqual:
          {
            // Only true is greater than or equal to true.
            if( value )
              return this.CompareEqual( expression, true );

            Type memberType = this.GetItemMemberType( expression.MemberName );

            // Only the null value is lower than false.
            if( memberType.IsNullableType() )
              return this.CompareNotEqualNull( expression );

            return FilterExpressionPreProcessorResult.TargetAll;
          }

        case FilterOperator.LessThan:
          {
            // Only true is greater than false.
            if( value )
              return this.CompareNotEqual( expression, true );

            Type memberType = this.GetItemMemberType( expression.MemberName );

            // Only the null value is lower than false.
            if( memberType.IsNullableType() )
              return this.CompareEqualNull( expression );

            return FilterExpressionPreProcessorResult.TargetNone;
          }

        case FilterOperator.LessThanOrEqual:
          {
            // No value is greater than true.
            if( value )
              return FilterExpressionPreProcessorResult.TargetAll;

            return this.CompareNotEqual( expression, true );
          }

        default:
          throw new InvalidOperationException( "An attempt was made to use an unsupported filter expression." );
      }
    }

    private FilterExpressionPreProcessorResult HandleComparableValue( FilterExpression expression )
    {
      if( expression.FilterOperator == FilterOperator.Equal )
      {
        return this.CompareEqual( expression, expression.Value );
      }
      else if( expression.FilterOperator == FilterOperator.NotEqual )
      {
        return this.CompareNotEqual( expression, expression.Value );
      }
      else
      {
        return this.Compare( expression );
      }
    }

    private FilterExpressionPreProcessorResult HandleEquatableValue( FilterExpression expression )
    {
      if( expression.FilterOperator == FilterOperator.Equal )
      {
        return this.CompareEqual( expression, expression.Value );
      }
      else if( expression.FilterOperator == FilterOperator.NotEqual )
      {
        return this.CompareNotEqual( expression, expression.Value );
      }

      throw new InvalidOperationException( "An attempt was made to use an unsupported filter expression." );
    }

    private FilterExpressionPreProcessorResult CompareEqualNull( FilterExpression expression )
    {
      return FilterExpressionPreProcessor.GetResult( expression, FilterOperator.Equal, null );
    }

    private FilterExpressionPreProcessorResult CompareNotEqualNull( FilterExpression expression )
    {
      return FilterExpressionPreProcessor.GetResult( expression, FilterOperator.NotEqual, null );
    }

    private FilterExpressionPreProcessorResult CompareEqual( FilterExpression expression, object value )
    {
      if( value == null )
        return this.CompareEqualNull( expression );

      FilterExpressionPreProcessorResult compare = FilterExpressionPreProcessor.GetResult( expression, FilterOperator.Equal, value );
      Type memberType = this.GetItemMemberType( expression.MemberName );

      if( !memberType.IsNullableStruct() )
        return compare;

      // Take care of a nullable member type.
      FilterExpressionPreProcessorResult handleNull = this.CompareNotEqualNull( expression );

      Debug.Assert( ( handleNull.Expression != null )
                 && ( compare.Expression != null ) );

      FilterExpressionBase newExpression = new AndFilterExpression( handleNull.Expression, compare.Expression );

      return new FilterExpressionPreProcessorResult( newExpression );
    }

    private FilterExpressionPreProcessorResult CompareNotEqual( FilterExpression expression, object value )
    {
      if( value == null )
        return this.CompareNotEqualNull( expression );

      FilterExpressionPreProcessorResult compare = FilterExpressionPreProcessor.GetResult( expression, FilterOperator.NotEqual, value );
      Type memberType = this.GetItemMemberType( expression.MemberName );

      if( !memberType.IsNullableStruct() )
        return compare;

      // Take care of a nullable member type.
      FilterExpressionPreProcessorResult handleNull = this.CompareEqualNull( expression );

      Debug.Assert( ( handleNull.Expression != null )
                 && ( compare.Expression != null ) );

      FilterExpressionBase newExpression = new OrFilterExpression( handleNull.Expression, compare.Expression );

      return new FilterExpressionPreProcessorResult( newExpression );
    }

    private FilterExpressionPreProcessorResult Compare( FilterExpression expression )
    {
      FilterExpressionBase newExpression = expression;
      Type memberType = this.GetItemMemberType( expression.MemberName );

      // Take care of a nullable member type.
      if( memberType.IsNullableType() )
      {
        switch( expression.FilterOperator )
        {
          case FilterOperator.GreaterThan:
          case FilterOperator.GreaterThanOrEqual:
            {
              FilterExpressionPreProcessorResult handleNull = this.CompareNotEqualNull( expression );

              newExpression = new AndFilterExpression( handleNull.Expression, newExpression );
            }
            break;

          case FilterOperator.LessThan:
          case FilterOperator.LessThanOrEqual:
            {
              FilterExpressionPreProcessorResult handleNull = this.CompareEqualNull( expression );

              newExpression = new OrFilterExpression( handleNull.Expression, newExpression );
            }
            break;

          case FilterOperator.StartsWith:
          case FilterOperator.EndsWith:
          case FilterOperator.Contains:
          case FilterOperator.ContainedIn:
            {
              if( typeof( string ) == memberType )
              {
                FilterExpressionPreProcessorResult handleNull = this.CompareNotEqualNull( expression );

                newExpression = new AndFilterExpression( handleNull.Expression, newExpression );
              }
            }
            break;

          default:
            break;
        }
      }

      return new FilterExpressionPreProcessorResult( newExpression );
    }

    private Type GetItemMemberType( string propertyName )
    {
      return m_getItemMemberType.Invoke( propertyName );
    }

    #region Private Fields

    private FilterExpressionPreProcessorResult m_result;
    private Func<string, Type> m_getItemMemberType;

    #endregion
  }
}
